package com.pingan.internal.point.shopping.service.impl;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.pingan.internal.point.common.domain.Record;
import com.pingan.internal.point.common.domain.User;
import com.pingan.internal.point.common.enums.OperateType;
import com.pingan.internal.point.common.enums.ResultCodeEnum;
import com.pingan.internal.point.common.exceptions.WrappedException;
import com.pingan.internal.point.common.model.ApiResultEntity;
import com.pingan.internal.point.common.model.params.ConsumeOperationsParam;
import com.pingan.internal.point.common.model.params.WithdrawOperationParam;
import com.pingan.internal.point.common.util.RetryExecutor;
import com.pingan.internal.point.shopping.common.enums.OrderStatus;
import com.pingan.internal.point.shopping.common.exception.BusinessException;
import com.pingan.internal.point.shopping.common.msg.ErrorMessage;
import com.pingan.internal.point.shopping.common.utils.DateUtils;
import com.pingan.internal.point.shopping.common.utils.GiftIdGenerate;
import com.pingan.internal.point.shopping.controller.form.GoodsDeliveryForm;
import com.pingan.internal.point.shopping.controller.form.GoodsFinishOrder;
import com.pingan.internal.point.shopping.controller.form.GoodsOrderForm;
import com.pingan.internal.point.shopping.controller.form.GoodsWithdrawForm;
import com.pingan.internal.point.shopping.controller.form.GoodsOrderListForm;
import com.pingan.internal.point.shopping.controller.response.DataPageList;
import com.pingan.internal.point.shopping.entity.Goods;
import com.pingan.internal.point.shopping.entity.GoodsOrder;
import com.pingan.internal.point.shopping.repository.GoodsOrderRepository;
import com.pingan.internal.point.shopping.repository.GoodsRepository;
import com.pingan.internal.point.shopping.repository.RecordsRepository;
import com.pingan.internal.point.shopping.service.GoodsOrderService;
import com.pingan.internal.point.shopping.vo.GoodsOrderVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.*;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static com.pingan.internal.point.common.util.SecurityContextUtil.getTokenFromContext;
import static com.pingan.internal.point.common.util.SecurityContextUtil.getUsernameFromContext;

@Service
public class GoodsOrderServiceImpl implements GoodsOrderService {
    private static final Logger LOGGER = LoggerFactory.getLogger(GoodsOrderServiceImpl.class);

    @Resource
    private GoodsOrderRepository goodsOrderRepository;

    @Resource
    private GoodsRepository goodsRepository;

    @Resource
    private RecordsRepository recordsRepository;

    @Autowired
    private RestTemplate restTemplate;

    @Value("${points.service.url}")
    private String points_service;

    @Value("${points.service.port}")
    private String port;

    @Value("${points.service.api.consume}")
    private String consume_api;

    @Value("${points.service.api.consume.withdraw}")
    private String consume_withdraw_api;

    @Value("${points.service.api.query.user}")
    private String query_user_api;

    @Override
    public DataPageList<GoodsOrderVo> list(String username) {
        String authUsername = getUsernameFromContext();
        if (!authUsername.equals(username)) {
            throw new WrappedException(ResultCodeEnum.TOKEN_NOT_MATCH);
        }
        DataPageList<GoodsOrderVo> data = new DataPageList<>();
        List<GoodsOrderVo> listVo = new ArrayList<>();
        List<GoodsOrder> list = goodsOrderRepository.findByUsername(username);
        list.forEach(order -> {
            GoodsOrderVo vo = new GoodsOrderVo();
            vo.withGoodsOrder(getGoodsOrder(order.getOrderSn()));
            vo.withGoods(getByGoodsSn(order.getGoodsSn()));
            vo.withPaymentRecord(getRecordById(order.getPaymentRecordId()));
            listVo.add(vo);
        });
        data.putList(listVo);
        return data;
    }

    //findAll(Example<S> var1, Pageable var2);

    @Override
    public DataPageList<GoodsOrderVo> listPage(GoodsOrderListForm form) {
        DataPageList<GoodsOrderVo> data = new DataPageList<>();
        GoodsOrder o = new GoodsOrder();
        if(!StringUtils.isEmpty(form.getUsername())){
            o.setUsername(form.getUsername());
        }
        if(!StringUtils.isEmpty(form.getOrderStatus())){
            o.setStatus(Integer.valueOf(form.getOrderStatus()));
        }
        o.setUsername(form.getUsername());
        ExampleMatcher matcher = ExampleMatcher.matching()
                .withIgnorePaths("id","quantity","version");
        if(StringUtils.isEmpty(form.getOrderStatus())){
            matcher = matcher.withIgnorePaths("status");
        }
        if(StringUtils.isEmpty(form.getUsername())){
            matcher = matcher.withIgnorePaths("username");
        }
        Example e = Example.of(o,matcher);
        Pageable p = PageRequest.of(form.getCurrentPage(),form.getSize());
        Page<GoodsOrder> page= goodsOrderRepository.findAll(e,p);
        List<GoodsOrderVo> listVo = new ArrayList<>();
        page.getContent().forEach(order->{
            GoodsOrderVo vo = new GoodsOrderVo();
            vo.withGoodsOrder(getGoodsOrder(order.getOrderSn()));
            vo.withGoods(getByGoodsSn(order.getGoodsSn()));
            vo.withPaymentRecord(getRecordById(order.getPaymentRecordId()));
            listVo.add(vo);
        });
        data.putList(listVo);
        data.setTotal(page.getTotalElements());
        data.setPageNum(page.getNumber());
        data.setSize(form.getSize());
        return data;
    }

    @Override
    public String generateOrder(GoodsOrderForm form) {
        Goods saved = processGoods(form);
        if (saved == null) {
            throw new RuntimeException("save order failed");
        }
        String orderSn = recordOrder(form);
        if (orderSn == null) {
            throw new WrappedException("生成订单失败", ResultCodeEnum.UNKNOWN_ERROR.getCode());
        }
        int totalPoints = saved.getGoodsPoints() * form.getQuantity();
        if (processPoints(form, orderSn, totalPoints)) {
            return orderSn;
        } else {
            throw new WrappedException("生成订单失败", ResultCodeEnum.UNKNOWN_ERROR.getCode());
        }
    }

    private boolean processPoints(GoodsOrderForm form, String orderSn, int totalPoints) {
        String recordId = consumePoints(form.getUsername(), totalPoints);
        if (recordId != null) {
            return enrichOrder(orderSn, recordId);
        }
        return false;
    }

    private Goods processGoods(GoodsOrderForm form) {
        try {
            return consumeGoods(form);
        } catch (ObjectOptimisticLockingFailureException e) {
            return RetryExecutor.submit(() -> consumeGoods(form), 3);
        }
    }

    private boolean enrichOrder(String orderSn, String recordId) {
        try {
            return enrichOrderWithRecordId(orderSn, recordId);
        } catch (ObjectOptimisticLockingFailureException e) {
            return RetryExecutor.submit(() -> enrichOrderWithRecordId(orderSn, recordId), 3);
        }
    }

    private boolean enrichOrderWithRecordId(String orderSn, String recordId) {
        GoodsOrder order = getGoodsOrder(orderSn);
        order.setPaymentRecordId(recordId);
        return goodsOrderRepository.save(order) != null;
    }

    private Goods getByGoodsSn(String goodsSn) {
        return goodsRepository.findByGoodsSn(goodsSn);
    }

    @Override
    public String withdrawOrder(GoodsWithdrawForm form) {
        GoodsOrder goodsOrder = cancelOrder(form);
        if (!rollbackGoods(goodsOrder.getGoodsSn(), goodsOrder.getQuantity())) {
            return null;
        }
        return withdrawConsumePoints(goodsOrder.getUsername(), goodsOrder.getPaymentRecordId());
    }

    private GoodsOrder cancelOrder(GoodsWithdrawForm form) {
        try {
            return cancel(form);
        } catch (ObjectOptimisticLockingFailureException e) {
            return RetryExecutor.submit(() -> cancel(form), 3);
        }
    }

    private GoodsOrder cancel(GoodsWithdrawForm form) {
        GoodsOrder order = getGoodsOrder(form.getOrderSn());
        order.setStatus(OrderStatus.CANCEL.getStatus());
        order.setOrderMessage(form.getMessage());
        return goodsOrderRepository.save(order);
    }

    private Boolean rollback(String goodsSn, int quantity) {
        Goods goods = getByGoodsSn(goodsSn);
        goods.setGoodsQuantity(goods.getGoodsQuantity() + quantity);
        goods.setExchangeCount(goods.getExchangeCount() - quantity);
        return goodsRepository.save(goods) != null;
    }

    private Boolean rollbackGoods(String goodsSn, int quantity) {
        try {
            return rollback(goodsSn, quantity);
        } catch (ObjectOptimisticLockingFailureException e) {
            return RetryExecutor.submit(() -> rollback(goodsSn, quantity), 3);
        }
    }

    private String withdrawConsumePoints(String username, String recordId) {
        checkRecordState(recordId);
        WithdrawOperationParam param = getWithdrawOperationParam(username, recordId);
        String consumeWithdrawApi = getConsumeWithdrawApi();
        LOGGER.info("call consume withdraw api: {}", consumeWithdrawApi);
        ApiResultEntity resultEntity = restTemplate.exchange(consumeWithdrawApi, HttpMethod.POST, getHttpEntityWithTokenHeaders(param), ApiResultEntity.class).getBody();
        if (!"200".equals(resultEntity.getCode())) {
            throw new WrappedException(resultEntity.getMessage(), resultEntity.getCode());
        }
        return jsonConvert(resultEntity.getData(), String.class);
    }

    private WithdrawOperationParam getWithdrawOperationParam(String username, String recordId) {
        WithdrawOperationParam param = new WithdrawOperationParam();
        param.setUmId(username);
        param.setOperateType(OperateType.WITHDRAW_CONSUME.getValue());
        param.setRecordId(recordId);
        param.setOperateTime(DateUtils.getCurrentDateTime());
        param.setOperator(getUsernameFromContext());
        return param;
    }

    private void checkRecordState(String recordId) {//TODO
        Record record = getRecordById(recordId);
        Assert.isTrue(record.getType().equalsIgnoreCase(OperateType.CONSUME_POINTS.getValue()), "非消费记录");
    }

    @Override
    public String finishOrder(GoodsFinishOrder form) {
        try {
            return finish(form);
        } catch (ObjectOptimisticLockingFailureException e) {
            return RetryExecutor.submit(() -> finish(form), 3);
        }
    }
    private String finish(GoodsFinishOrder form) {
        GoodsOrder order = getGoodsOrder(form.getOrderSn());
        if(order.getStatus() != 1) {
            throw new BusinessException(ErrorMessage.ORDER_STATUS_ERROR);
        }
        order.setStatus(OrderStatus.DONE.getStatus());
        order.setOrderMessage(form.getMessage());
        return goodsOrderRepository.save(order) != null ? goodsOrderRepository.save(order).getOrderSn() : null;
    }

    @Override
    public String deliverOrder(GoodsDeliveryForm form) {
        try {
            return deliver(form);
        } catch (ObjectOptimisticLockingFailureException e) {
            return RetryExecutor.submit(() -> deliver(form), 3);
        }
    }

    private String deliver(GoodsDeliveryForm form) {
        GoodsOrder order = getGoodsOrder(form.getOrderSn());
        if(order.getStatus() > 0) {
            throw new BusinessException(ErrorMessage.ORDER_STATUS_ERROR);
        }
        order.setStatus(OrderStatus.SHIPMENT.getStatus());
        order.setShipTime(DateUtils.getCurrentDateTime());
        order.setOrderMessage(form.getShipMessage());
        return goodsOrderRepository.save(order) != null ? goodsOrderRepository.save(order).getOrderSn() : null;
    }

    private String recordOrder(GoodsOrderForm form) {
        GoodsOrder order = new GoodsOrder();
        order.setOrderSn(new GiftIdGenerate().toHexString());
        order.setGoodsSn(form.getGoodsSn());
        order.setQuantity(form.getQuantity());

        order.setUsername(form.getUsername());
        order.setRealName(form.getRealName());
        order.setAddress(form.getAddress());
        order.setCellphoneNumber(form.getCellphoneNumber());
        order.setUserMessage(form.getUserMessage());

        order.setStatus(OrderStatus.PENDING.getStatus());

        return goodsOrderRepository.save(order).getOrderSn();
    }

    private String consumePoints(String username, int totalPoints) {
        ConsumeOperationsParam param = getConsumeOperationsParam(username, totalPoints);
        String consumeApi = getConsumeApi();
        LOGGER.info("call consume api: {}", consumeApi);
        ApiResultEntity resultEntity = restTemplate.exchange(consumeApi, HttpMethod.POST, getHttpEntityWithTokenHeaders(param), ApiResultEntity.class).getBody();
        if (!"200".equals(resultEntity.getCode())) {
            throw new WrappedException(resultEntity.getMessage(), resultEntity.getCode());
        }
        return jsonConvert(resultEntity.getData(), String.class);
    }

    private ConsumeOperationsParam getConsumeOperationsParam(String username, int totalPoints) {
        ConsumeOperationsParam param = new ConsumeOperationsParam();
        param.setUmId(username);
        param.setOperateType(OperateType.CONSUME_POINTS.getValue());
        param.setPoints(totalPoints);
        param.setOperateTime(DateUtils.getCurrentDateTime());
        param.setOperator(getUsernameFromContext());
        return param;
    }

    private Goods consumeGoods(GoodsOrderForm form) {
        Goods goods = getByGoodsSn(form.getGoodsSn());
        checkInventoryState(form, goods);
        checkUserState(form, goods);
        goods.setGoodsQuantity(goods.getGoodsQuantity() - form.getQuantity());
        goods.setExchangeCount(goods.getExchangeCount() + form.getQuantity());
        return goodsRepository.save(goods);
    }

    private void checkUserState(GoodsOrderForm form, Goods goods) {
        String queryUserApi = getQueryUserApi();
        LOGGER.info("call query user api: {}", queryUserApi);
        ApiResultEntity resultEntity = restTemplate.exchange(queryUserApi, HttpMethod.GET, getHttpEntityWithTokenHeaders(null), ApiResultEntity.class, form.getUsername()).getBody();
        if (resultEntity.getData() == null) {
            return;
        }
        User user = jsonConvert(resultEntity.getData(), User.class);
        int totalPoints = goods.getGoodsPoints() * form.getQuantity();
        Assert.isTrue(user.getCurrentPoints() > totalPoints, "分数不够");
    }

    private static HttpEntity<Object> getHttpEntityWithTokenHeaders(Object requestBody) {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + getTokenFromContext());
        return new HttpEntity<>(requestBody, headers);
    }

    private void checkInventoryState(GoodsOrderForm form, Goods goods) {
        if (goods.getLimitType()) {
            Assert.isTrue(goods.getLimitCount() >= form.getQuantity(), "购买数量超过限制");
        }
        Assert.isTrue(goods.getGoodsQuantity() >= form.getQuantity(), "商品数量不足");
    }

    private String getConsumeApi() {
        return points_service + ":" + port + consume_api;
    }

    private String getConsumeWithdrawApi() {
        return points_service + ":" + port + consume_withdraw_api;
    }

    private String getQueryUserApi() {
        return points_service + ":" + port + query_user_api;
    }

    private <T> T jsonConvert(Object value, Class<T> tClass) {
        ObjectMapper mapper = new ObjectMapper();
        return mapper.convertValue(value, tClass);
    }

    private GoodsOrder getGoodsOrder(String orderSn) {
        return goodsOrderRepository.findByOrderSn(orderSn);
    }

    private Record getRecordById(String recordId) {
        Optional<Record> record = recordsRepository.findById(Long.parseLong(recordId));
        if (record.isPresent()) {
            return record.get();
        }
        return null;
    }

}
